/*
 * Copyright (C) 2025 Huawei Device Co., Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
#ifndef HTTP2_HTTP2_COMMON_H
#define HTTP2_HTTP2_COMMON_H

#include "http2_handle_shake.h"

using namespace std;

enum { IO_NONE, WANT_READ, WANT_WRITE };

void (*g_postEvent)(int type, void *data, int len, int reqType, string requestId);

void MessageCallback(void (*postEventFunction)(int type, void *data, int len, int reqType, string requestId))
{
    g_postEvent = postEventFunction;
}

ssize_t SendCallback(nghttp2_session *session, const uint8_t *data, size_t length, int flags, void *userData)
{
    struct Connection *connection = nullptr;
    int rv = 0;
    connection = (struct Connection *)userData;
    connection->wantIo = IO_NONE;
    LOGE("SendCallback reqId:%s", connection->reqId.c_str());
    if (IsDestroyed(connection->reqId)) {
        return NGHTTP2_ERR_CALLBACK_FAILURE;
    }
    ERR_clear_error();
    int len = static_cast<int>(length);
    rv = SSL_write(connection->ssl, data, len);
    if (rv <= 0) {
        int err = SSL_get_error(connection->ssl, rv);
        if (err == SSL_ERROR_WANT_WRITE || err == SSL_ERROR_WANT_READ) {
            connection->wantIo = (err == SSL_ERROR_WANT_READ ? WANT_READ : WANT_WRITE);
            return NGHTTP2_ERR_WOULDBLOCK;
        } else {
            return NGHTTP2_ERR_CALLBACK_FAILURE;
        }
    }
    return rv;
}

ssize_t RecvCallback(nghttp2_session *session, uint8_t *buf, size_t length, int flags, void *userData)
{
    int rv = 0;
    struct Connection *connection = (struct Connection *)userData;
    LOGE("RecvCallback %d reqId %s", length, connection->reqId.c_str());
    connection->wantIo = IO_NONE;
    if ((connection == nullptr) || (connection->ssl == nullptr) ||
        (buf == nullptr) || IsDestroyed(connection->reqId)) {
        return 0;
    }
    ERR_clear_error();
    int len = static_cast<int>(length);
    rv = SSL_read(connection->ssl, buf, len);
    LOGE("RecvCallback SSL_read %d", rv);
    if (rv < 0) {
        int err = SSL_get_error(connection->ssl, rv);
        if (err == SSL_ERROR_WANT_WRITE || err == SSL_ERROR_WANT_READ) {
            connection->wantIo = (err == SSL_ERROR_WANT_READ ? WANT_READ : WANT_WRITE);
            rv = NGHTTP2_ERR_WOULDBLOCK;
        } else {
            rv = NGHTTP2_ERR_CALLBACK_FAILURE;
        }
    } else if (rv == 0) {
        rv = NGHTTP2_ERR_EOF;
        LOGE("recv_callback NGHTTP2_ERR_EOF");
    }
    return rv;
}


void HandleGoawayFrame(void *userData, nghttp2_goaway goway)
{
    struct Connection *connection = (Connection *)userData;
    LOGE("HandleGoawayFrame fd:%d data:%s", connection->fd, (char *)goway.opaque_data);
    std::unordered_map<char *, void *> map;
    map["opaque_data"] = goway.opaque_data;
    map["error_code"] = (int *)goway.error_code;
    map["last_stream_id"] = (int *)goway.last_stream_id;
    map["fd"] = (int *)connection->fd;
    g_postEvent(HTTP_STREAM_CALLBACK_TYPE_GOWAY, &map, 0, connection->reqType, connection->reqId);
}

int OnFrameRecvCallback(nghttp2_session *session, const nghttp2_frame *frame, void *userData)
{
    struct Connection *connection = nullptr;
    connection = (struct Connection *)userData;
    LOGE("OnFrameRecvCallback %d reqId %s", frame->hd.type, connection->reqId.c_str());
    if (IsDestroyed(connection->reqId)) {
        return 0;
    }
    if (frame->hd.type == NGHTTP2_PING) {
        bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
        LOGE("OnFrameRecvCallback ping ack %d reqId %s", ack, connection->reqId.c_str());
        if (ack) {
            int pingRv = nghttp2_submit_ping(connection->session, NGHTTP2_FLAG_ACK, frame->ping.opaque_data);
            LOGE("OnFrameRecvCallback ping ack result %d reqId %s", pingRv, connection->reqId.c_str());
        }
    }
    
    if (frame->hd.type == NGHTTP2_DATA) {
        LOGE("OnFrameRecvCallback NGHTTP2_DATA reqId %s ", connection->reqId.c_str());
        g_postEvent(HTTP_STREAM_CALLBACK_TYPE_RESPONSE_DATA_TRUNCK,
            connection->responseData.data(), connection->responseData.size(), connection->reqType, connection->reqId);
        connection->responseData.clear();
    }
    return 0;
}

int OnStreamCloseCallback(nghttp2_session *session, int32_t streamId, uint32_t errorCode, void *userData)
{
    LOGE("OnStreamCloseCallback");
    struct Connection *connection = nullptr;
    connection = static_cast<struct Connection *>(userData);
    if (IsDestroyed(connection->reqId)) {
        return 0;
    }
    LOGE("OnStreamCloseCallback errcode %d reqId %s", errorCode, connection->reqId.c_str());
    if (g_responseHeadersManagerMap.find(connection->reqId) == g_responseHeadersManagerMap.end()) {
        return 0;
    }
    auto range = g_responseHeadersManagerMap.equal_range(connection->reqId);
    std::unordered_map<std::string, std::string> resultMap;
    for (auto it = range.first; it != range.second; ++it) {
        const auto& innerMap = it->second;
        resultMap.insert(innerMap.begin(), innerMap.end());
    }
    g_responseHeadersManagerMap.erase(connection->reqId);
    nlohmann::json headerJson = resultMap;
    const string headerString = headerJson.dump();
    resultMap.clear();
    g_postEvent(HTTP_STREAM_CALLBACK_TYPE_RESPONSE, const_cast<char *>(headerString.c_str()),
        0, connection->reqType, connection->reqId);
    usleep(NUM_100);
    g_postEvent(HTTP_CALLBACK_TYPE_CLOSE, const_cast<char *>(std::to_string(errorCode).c_str()),
        0, connection->reqType, connection->reqId);
    usleep(NUM_100);
    g_postEvent(HTTP_STREAM_CALLBACK_TYPE_END, nullptr, 0, connection->reqType, connection->reqId);
    g_streamCloseManagerMap[connection->reqId] = true;
    LOGE("OnStreamCloseCallback success header %s reqId %s ", headerString.c_str(), connection->reqId.c_str());
    return 0;
}

int OnDataChunkRecvCallback(nghttp2_session *session, uint8_t flags, int32_t streamId, const uint8_t *data,
                            size_t len, void *userData)
{
    struct Connection *connection = nullptr;
    connection = (struct Connection *)userData;
    LOGE("OnDataChunkRecvCallback len:%d reqId:%s", len, connection->reqId.c_str());
    if (IsDestroyed(connection->reqId)) {
        return 0;
    }
    connection->responseData.insert(connection->responseData.end(), data, data + len);
    LOGE("OnDataChunkRecvCallback reqType %d flags %d", connection->reqType, flags);
    return 0;
}

int OnHeaderCallback(nghttp2_session *handle, const nghttp2_frame *frame, nghttp2_rcbuf *name,
                     nghttp2_rcbuf *value, uint8_t flags, void *userData)
{
    LOGE("OnHeaderCallback name %s value %s flags %d", nghttp2_rcbuf_get_buf(name).base,
         nghttp2_rcbuf_get_buf(value).base, flags);
    struct Connection *connection = nullptr;
    connection = (struct Connection *)userData;
    if (IsDestroyed(connection->reqId)) {
        return 0;
    }
    std::unordered_map<std::string, std::string> headerMap;
    headerMap[(char *)nghttp2_rcbuf_get_buf(name).base] = (char *)nghttp2_rcbuf_get_buf(value).base;
    g_responseHeadersManagerMap.insert({connection->reqId, headerMap});
    return 0;
}

int OnNghttpError(nghttp2_session *handle, int errorCode, const char *message, size_t len,
                  void *userData)
{
    struct Connection *connection = nullptr;
    connection = (struct Connection *)userData;
    if (IsDestroyed(connection->reqId)) {
        return 0;
    }
    LOGE("OnNghttpError errcode %d msg %s reqId %s", errorCode, message, connection->reqId.c_str());
    HttpStreamErrorCallback errCallBack;
    errCallBack.errCode = errorCode;
    errCallBack.msg = message;
    std::unordered_map<std::string, std::string> errorMapInner;
    errorMapInner["message"] = (char *)message;
    errorMapInner["code"] = (char *)std::to_string(errorCode).c_str();
    std::string result = UnorderedMapToJsonString(errorMapInner);
    void* data = static_cast<void *>(const_cast<char*>(result.c_str()));
    g_postEvent(HTTP_STREAM_CALLBACK_TYPE_ERROR, data, 0, connection->reqType, connection->reqId);
    g_streamCloseManagerMap[connection->reqId] = true;
    return 0;
}

void SetupNghttp2Callbacks(nghttp2_session_callbacks *callbacks)
{
    nghttp2_session_callbacks_set_on_header_callback2(callbacks, OnHeaderCallback);
    nghttp2_session_callbacks_set_send_callback(callbacks, SendCallback);
    nghttp2_session_callbacks_set_recv_callback(callbacks, RecvCallback);
    nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, OnFrameRecvCallback);
    nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, OnStreamCloseCallback);
    nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, OnDataChunkRecvCallback);
    nghttp2_session_callbacks_set_error_callback2(callbacks, OnNghttpError);
}

void CtlPoll(struct pollfd *pollfd, nghttp2_session *session, int wantIo)
{
    pollfd->events = 0;
    if (nghttp2_session_want_read(session) || wantIo == WANT_READ) {
        pollfd->events |= POLLIN;
    }
    if (nghttp2_session_want_write(session) || wantIo == WANT_WRITE) {
        pollfd->events |= POLLOUT;
    }
}

HandleIoResult HandleIo(nghttp2_session *session, string reqId)
{
    LOGE("handle_io session %p reqId %s", session, reqId.c_str());
    HandleIoResult handleIoResult;
    if (!session || IsDestroyed(reqId)) {
        handleIoResult.code = 0;
        return handleIoResult;
    }
    int sessionResult = 0;
    sessionResult = nghttp2_session_recv(session);
    if (sessionResult != 0) {
        LOGE("nghttp2_session_recv %d reqId %s ", sessionResult, reqId.c_str());
        handleIoResult.code = sessionResult;
        handleIoResult.isRecv = true;
        return handleIoResult;
    }
    sessionResult = nghttp2_session_send(session);
    if (sessionResult != 0) {
        LOGE("nghttp2_session_send %d reqId %s", sessionResult, reqId.c_str());
        handleIoResult.code = sessionResult;
        handleIoResult.isRecv = false;
        return handleIoResult;
    }
    LOGE("handle_io session end sessionResult %d reqId %s", sessionResult, reqId.c_str());
    handleIoResult.code = sessionResult;
    return handleIoResult;
}


ssize_t DataSourceReadCallback(nghttp2_session *session, int32_t streamId, uint8_t *buf, size_t len,
                               uint32_t *dataFlags, nghttp2_data_source *source, void *userData)
{
    LOGE("DataSourceReadCallback start");
    uint8_t *buffer = static_cast<uint8_t *>(source->ptr);
    Connection *connection = (struct Connection *)userData;
    *dataFlags |= NGHTTP2_DATA_FLAG_EOF;
    LOGE("DataSourceReadCallback write_buff_len %d reqId %s fd %d", connection->writeBuffLen,
        connection->reqId.c_str(), connection->fd);
    if (CheckReqId(connection->reqId) && (connection->fd > 0 && connection->fd < NUM_1000)) {
        std::copy(buffer, buffer + connection->writeBuffLen, buf);
        return connection->writeBuffLen;
    } else {
        LOGE("Error DataSourceReadCallback");
        return 0;
    }
}

void SendData(Connection *connection, void *bufferData, int buffLen, string extendParams)
{
    LOGE("napi--->sendData reqId %s host %s path %s buffLen %d", connection->reqId.c_str(),
         connection->host.c_str(), connection->path.c_str(), buffLen);
    std::unordered_map<std::string, std::string> parseHeadersMap = ParseReqExtendParams(extendParams);
    std::vector<nghttp2_nv> nva;
    nva.push_back(CreateNV(":method", "POST"));
    nva.push_back(CreateNV(":path", connection->path.c_str()));
    nva.push_back(CreateNV(":scheme", "https"));
    nva.push_back(CreateNV(":authority", connection->host.c_str()));
    nva.push_back(CreateNV("content-type", "application/grpc"));
    nva.push_back(CreateNV("accept", "*/*"));
    nva.push_back(CreateNV("te", "trailers"));
    nva.push_back(CreateNV("accept-encoding", "identity"));
    nva.push_back(CreateNV("grpc-accept-encoding", "identity,deflate,gzip"));
    nva.push_back(CreateNV("user-agent", "grpc-node-js/0.1.0" NGHTTP2_VERSION));
    for (const auto &header : parseHeadersMap) {
        const std::string key = header.first;
        const std::string value = header.second;
        LOGE("napi--->sendData header key %s value %s", key.c_str(), value.c_str());
        if (CheckStringIsArray(value)) {
            const std::string valueTemp = GetFirstIndexFromArray(value);
            nva.push_back(CreateNV(key.c_str(), valueTemp.c_str()));
        } else {
            nva.push_back(CreateNV(key.c_str(), value.c_str()));
        }
    }
    nghttp2_data_source reqBuffer;
    reqBuffer.ptr = bufferData;
    nghttp2_data_provider data_prvd = {reqBuffer, DataSourceReadCallback};
    connection->writeBuffLen = buffLen;
    connection->reqType = 1;
    int streamId = nghttp2_submit_request(connection->session, nullptr, nva.data(), nva.size(), &data_prvd, connection);
    if (streamId < 0) {
        LOGE("sendData stream_id_ %d", streamId);
    }
}

int HandleSessionClient(Connection *connection)
{
    nghttp2_session_callbacks *callbacks;
    int rv = nghttp2_session_callbacks_new(&callbacks);
    if (rv != 0) {
        LOGE("nghttp2_session_callbacks_new %d", rv);
        return rv;
    }
    SetupNghttp2Callbacks(callbacks);
    nghttp2_option *options;
    nghttp2_option_new(&options);
    nghttp2_option_set_max_send_header_block_length(options, NUM_65535);
    nghttp2_option_set_no_closed_streams(options, NUM_1);
    nghttp2_option_set_no_auto_window_update(options, NUM_1);
    connection->callbacks = callbacks;
    rv = nghttp2_session_client_new3(&connection->session, callbacks, connection, options, nullptr);
    if (rv != 0) {
        LOGE("nghttp2_session_client_new %d", rv);
        nghttp2_session_callbacks_del(callbacks);
        return rv;
    }
    return rv;
}

int HandleSSLShake(Connection *connection, int fd, string hostAddress)
{
    SSL_CTX *sslCtx;
    SSL *ssl;
    sslCtx = SSL_CTX_new(TLS_client_method());
    if (sslCtx == nullptr) {
        LOGE("SSL_CTX_new %s", ERR_error_string(ERR_get_error(), nullptr));
    }
    InitSslCtx(sslCtx);
    ssl = SSL_new(sslCtx);
    if (ssl == nullptr) {
        LOGE("SSL_new %s", ERR_error_string(ERR_get_error(), nullptr));
    }
    int rv = OpenSSLConnectHandle(ssl, fd, hostAddress);
    if (rv < 0) {
        LOGE("SSL/TLS handshake failed rv %d", rv);
        return rv;
    }
    connection->ssl = ssl;
    connection->sslCtx = sslCtx;
    connection->wantIo = IO_NONE;
    connection->fd = fd;
    MakeNonBlock(fd);
    SetTcpNodelay(fd);
    rv = HandleSessionClient(connection);
    if (rv != 0) {
        LOGE("HandleSessionClient %d", rv);
        return rv;
    }
    nghttp2_settings_entry iv[] = {
        {NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, NUM_10 * NUM_1024 * NUM_1024},
        {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, NUM_100}
    };
    rv = nghttp2_submit_settings(connection->session, NGHTTP2_FLAG_NONE, iv, sizeof(iv)/sizeof(iv[0]));
    if (rv != 0) {
        LOGE("nghttp2_submit_settings %d", rv);
        return rv;
    }
    return rv;
}

void FreeMapByReqId(string reqId)
{
    LOGE("FreeMapByReqId reqId %s", reqId.c_str());
    if (g_callbackManagerMap.find(reqId) != g_callbackManagerMap.end()) {
        auto callbackRef = g_callbackManagerMap.at(reqId);
        napi_delete_reference(g_envManagerMap.at(reqId), callbackRef);
        g_callbackManagerMap.erase(reqId);
    }
    if (g_envManagerMap.find(reqId) != g_envManagerMap.end()) {
        g_envManagerMap.erase(reqId);
    }
    if (g_streamCloseManagerMap.find(reqId) != g_streamCloseManagerMap.end()) {
        g_streamCloseManagerMap.erase(reqId);
    }
    if (g_reqManagerMap.find(reqId) != g_reqManagerMap.end()) {
        g_reqManagerMap.erase(reqId);
    }
    if (g_requestManagerMap.find(reqId) != g_requestManagerMap.end()) {
        delete g_requestManagerMap.at(reqId);
        g_requestManagerMap.erase(reqId);
    }
    if (g_connectManagerMap.find(reqId) != g_connectManagerMap.end()) {
        g_connectManagerMap.erase(reqId);
    }
    LOGE("FreeMapByReqId success reqId %s", reqId.c_str());
}

void FreeResource(Connection *connection)
{
    if (!connection || connection->fd <= 0 || connection->fd > NUM_1000 ||
        connection->reqId.empty() || !CheckReqId(connection->reqId)) {
        return;
    }
    g_sessionManagerMap[connection->reqId] = 1;
    LOGE("FreeResource fd %d reqId %s reqIdLen %d", connection->fd,
         connection->reqId.c_str(), connection->reqId.length());
    if (g_freeManagerMap.find(connection->reqId) != g_freeManagerMap.end()) {
        LOGE("FreeResource already released reqId %s", connection->reqId.c_str());
        g_freeManagerMap.erase(connection->reqId);
        return;
    }
    g_freeManagerMap[connection->reqId] = true;
    usleep(NUM_100 * NUM_1000);
    if (connection->callbacks) {
        nghttp2_session_callbacks_del(connection->callbacks);
    }
    if (connection->session) {
        nghttp2_session_del(connection->session);
    }
    if (connection->ssl) {
        SSL_shutdown(connection->ssl);
        SSL_free(connection->ssl);
    }
    if (connection->fd > 0) {
        shutdown(connection->fd, SHUT_WR);
        close(connection->fd);
    }
    connection->responseData.clear();
    connection->responseData.shrink_to_fit();
    LOGE("FreeResource success fd %d reqId %s", connection->fd, connection->reqId.c_str());
    FreeMapByReqId(connection->reqId);
}

void SetCallBackRef(string reqId, const napi_ref &callbackRef)
{
    LOGE("setCallBackRef reqId %s callbackRef %p", reqId.c_str(), callbackRef);
    g_callbackManagerMap[reqId] = callbackRef;
}

void SetEnvMessage(string requestID, const napi_env &env)
{
    LOGE("setEnvMessage reqId %s env %p", requestID.c_str(), env);
    g_envManagerMap[requestID] =  env;
}

void FreeRequestCallback(RequestCallback *requestCallback)
{
    delete requestCallback;
    requestCallback = nullptr;
}

void EndStreamTask(napi_env env, void *data)
{
    Connection *connection = (Connection *)data;
    napi_value result;
    napi_create_string_utf8(env, connection->reqId.c_str(), NAPI_AUTO_LENGTH, &result);
    napi_resolve_deferred(connection->env, connection->deferred, result);
    napi_delete_async_work(env, connection->asyncWork);
    delete connection;
    connection = nullptr;
}

void EndWriteTask(napi_env env, void *data)
{
    RequestTask *requestTask = (RequestTask *)data;
    napi_value result;
    napi_create_string_utf8(env, requestTask->reqId.c_str(), NAPI_AUTO_LENGTH, &result);
    napi_resolve_deferred(requestTask->env, requestTask->deferred, result);
    napi_delete_async_work(env, requestTask->asyncWork);
    delete requestTask;
    requestTask = nullptr;
}

#endif